// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016-2019, LomboTech Co.Ltd.
 * Authors:
 *	lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute  it and/or modify it
 * under  the terms of  the GNU General  Public License as published by the
 * Free Software Foundation;  either version 2 of the  License, or (at your
 * option) any later version.
 */

#include <drm/drmP.h>
#include <linux/clk.h>
#include <linux/component.h>

#include "csp_dpu_top.h"
#include "lombo_dpu.h"
#include "lombo_drv.h"

#define LOMBO_DPU_EXT_SCLK_NUM 3
#define LOMBO_DPU_SCLK_NUM 3
#define LOMBO_DPU_MOD_TIMER 0xe
#define LOMBO_DPU_MOD_TOP 31

/**
 * @ext_sclk: dpu_sck(x)_ext. this sclk is invalid if @ext_sclk is null.
 * @id: sclk id.
 * @req_rate: request rate.
 * @real_rate: real rate of this sclk.
 * @use_adpll: whether apply inner-adpll.
 * @adpll_cfg: configure of adpll.
 * @mlock: lock for enable/disable this sclk.
 * @en_cnt: enable count.
 */
struct lombo_dpu_sclk {
	uint8_t id;

	uint8_t use_adpll;
	struct dpu_adpll_config adpll_cfg;

	struct clk *ext_sclk;

	unsigned long req_rate;
	unsigned long real_rate;

	struct mutex mlock;
	int en_cnt;
};

/**
 * @mod_id: module id, see enum LOMBO_DPU_MOD_.
 * @sclk: source clock for module clock.
 * @req_rate: request rate.
 * @real_rate: the actual rate after enabled.
 * @ref_cnt: referent count.
 * @list: list node to dpu_clk_list.
 * @slock: lock for enable/disable.
 * @en_cnt: enable count.
 */
struct lombo_dpu_clk {
	uint32_t mod_id;

	struct lombo_dpu_top *top;
	struct lombo_dpu_sclk *sclk;

	unsigned long req_rate;
	unsigned long real_rate;

	struct kref ref_cnt;
	struct list_head list;

	spinlock_t slock;
	int en_cnt;
};

struct lombo_dpu_top {
	void __iomem *reg;

	struct clk *ahb_gate;
	struct clk *ahb_reset;
	struct clk *mem_axi_gate;

	struct clk *ext_sclk[LOMBO_DPU_EXT_SCLK_NUM];
	struct clk *ext_psclk[LOMBO_DPU_EXT_SCLK_NUM];
	struct lombo_dpu_sclk sclk[LOMBO_DPU_SCLK_NUM];

	/* list of module clks */
	struct list_head clk_list;

	/* for dpu top access */
	spinlock_t slock;
	int en_cnt;
};

/**
 * @dpu_mod: see enum LOMBO_DPU_MOD_x
 */
struct lombo_dpu_clk *lombo_dpu_get_clk(struct device *dpu_top,
	struct device *mod, unsigned int dpu_mod, unsigned int sclk_id)
{
	struct lombo_dpu_top *top = NULL;
	struct lombo_dpu_sclk *sclk = NULL;
	struct lombo_dpu_clk *mod_clk = NULL;
	uint32_t val = 0;

	if (!mod || !dpu_top) {
		DRM_ERROR("null pointer, mod=%p, dpu_top=%p\n", mod, dpu_top);
		return NULL;
	}
	if (dpu_mod > LOMBO_DPU_MOD_TOP) {
		DRM_DEV_ERROR(mod, "inval dpu_mode=%d\n", dpu_mod);
		return NULL;
	}

	if (sclk_id == LOMBO_DPU_SCLK_DEF) {
		struct device_node *np = mod->of_node;
		uint32_t clk_val[2] = { 0, 0 };

		if (!of_property_read_u32_array(np, "clk", clk_val,
						ARRAY_SIZE(clk_val))) {
			sclk_id = clk_val[0];
			val = clk_val[1]; /* clk_rate */
		} else {
			DRM_DEV_INFO(mod, "no clk property\n");
			if (!of_property_read_u32(np, "sclk", &val))
				sclk_id = val;
			if (of_property_read_u32(np, "clk_rate", &val)) {
				val = 0;
				DRM_DEV_ERROR(mod, "failed to get clk_rate\n");
			}
		}

		if (sclk_id == LOMBO_DPU_SCLK_DEF)
			sclk_id = 0; /* default sclk0 */

		DRM_DEV_DEBUG_DRIVER(mod, "mod=%d: sclk%u,req_rate=%u\n",
				     dpu_mod, sclk_id, val);
	}
	if (sclk_id >= LOMBO_DPU_SCLK_NUM) {
		DRM_DEV_ERROR(mod, "invalid sclk_id=%d. need < %d\n",
			      sclk_id, LOMBO_DPU_SCLK_NUM);
		return NULL;
	}

	top = dev_get_drvdata(dpu_top);
	if (!top) {
		DRM_DEV_ERROR(dpu_top, "dpu_top is null\n");
		return NULL;
	}

	/* assume sclk must has ext_sclk */
	sclk = &top->sclk[sclk_id];
	if (!sclk->ext_sclk) {
		DRM_DEV_ERROR(dpu_top, "sclk%d not ext_sclk\n", sclk_id);
		return NULL;
	}

	/**
	 * here not use spinlock for clk_list, the reason is:
	 * lombo_dpu_get_clk should be called by moules in driver-bind.
	 * Mouldes are binded in sequence.
	 */
	list_for_each_entry(mod_clk, &top->clk_list, list) {
		if (mod_clk->mod_id == dpu_mod) {
			if (mod_clk->sclk != sclk) {
				DRM_DEV_ERROR(mod, "no match sclk%d\n",
					      sclk_id);
				return NULL;
			}
			kref_get(&mod_clk->ref_cnt);
			return mod_clk;
		}
	}

	mod_clk = kzalloc(sizeof(*mod_clk), GFP_KERNEL);
	if (!mod_clk) {
		DRM_DEV_ERROR(dpu_top, "alloc for dpu_clk struct fail\n");
		return NULL;
	}

	mod_clk->top = top;
	mod_clk->sclk = sclk;
	mod_clk->mod_id = dpu_mod;
	mod_clk->req_rate = val;
	kref_init(&mod_clk->ref_cnt);
	spin_lock_init(&mod_clk->slock);
	list_add_tail(&mod_clk->list, &top->clk_list);

	return mod_clk;
}

static void lombo_dpu_clk_free(struct kref *kref)
{
	struct lombo_dpu_clk *dpu_clk =
		container_of(kref, struct lombo_dpu_clk, ref_cnt);

	list_del(&dpu_clk->list);
	if (dpu_clk->en_cnt > 0)
		DRM_ERROR("dpu_mod=%d not disabled\n", dpu_clk->mod_id);

	kfree(dpu_clk);
}

int lombo_dpu_put_clk(struct lombo_dpu_clk **clk)
{
	struct lombo_dpu_clk *dpu_clk;

	if (!(clk && (*clk))) {
		DRM_ERROR("%d\n", __LINE__);
		return -EINVAL;
	}

	dpu_clk = *clk;
	*clk = NULL;
	kref_put(&dpu_clk->ref_cnt, lombo_dpu_clk_free);

	return 0;
}

unsigned long lombo_dpu_clk_get_rate(struct lombo_dpu_clk *clk, int which)
{
	if (!clk)
		return 0;

	switch (which) {
	case LOMBO_DPU_CLK_REQ_RATE:
		return clk->req_rate;
	case LOMBO_DPU_CLK_REAL_RATE:
		return clk->real_rate;
	case LOMBO_DPU_CLK_PARENT_RATE:
		if (clk->sclk)
			return clk->sclk->real_rate;
		return 0;
	default:
		return 0;
	}
}

static int lombo_dpu_clk_set_rate_do_sync(struct lombo_dpu_clk *clk,
	unsigned long rate)
{
	int en_cnt;

	spin_lock(&clk->slock);
	if ((clk->en_cnt > 0) && (rate != clk->req_rate)) {
		struct lombo_dpu_top *top = clk->top;
		struct lombo_dpu_sclk *sclk = clk->sclk;
		uint64_t tmp;
		uint32_t div;

		/* calc div */
		tmp = sclk->real_rate;
		do_div(tmp, rate);
		div = tmp ? (uint32_t)tmp : 1;

		spin_lock(&top->slock);
		csp_dpu_clk_disable(clk->mod_id);
		csp_dpu_clk_set_divider(clk->mod_id, div);
		csp_dpu_clk_enable(clk->mod_id);
		spin_unlock(&top->slock);

		tmp = sclk->real_rate;
		do_div(tmp, div);
		clk->real_rate = (unsigned long)tmp;
	}
	clk->req_rate = rate;
	en_cnt = clk->en_cnt;
	spin_unlock(&clk->slock);

	WARN_ON(en_cnt <= 0);
	return 0;
}

int lombo_dpu_clk_set_rate_g(struct lombo_dpu_clk *clk,
	unsigned long rate, int sync)
{
	if (!clk)
		return -EINVAL;

	if (!sync) {
		clk->req_rate = rate;
		return 0;
	}

	if (!rate)
		return -EINVAL;
	return lombo_dpu_clk_set_rate_do_sync(clk, rate);
}

static inline void lombo_dpu_top_clk_assert(
	struct lombo_dpu_top *top, int sw)
{
	spin_lock(&top->slock);
	top->en_cnt++;
	if (!sw && (top->en_cnt == 1))
		csp_dpu_clk_assert(DPU_MOD_TOP);
	spin_unlock(&top->slock);
}

static inline void lombo_dpu_top_clk_deassert(
	struct lombo_dpu_top *top, int sw)
{
	spin_lock(&top->slock);
	top->en_cnt--;
	if (!sw && (top->en_cnt == 0))
		csp_dpu_clk_deassert(DPU_MOD_TOP);
	spin_unlock(&top->slock);
}

static int lombo_dpu_enable_sclk(struct lombo_dpu_top *top,
	struct lombo_dpu_sclk *sclk, int sw)
{
	unsigned long real_rate;
	int en_cnt;
	int ret = 0;

	mutex_lock(&sclk->mlock);
	sclk->en_cnt++;
	en_cnt = sclk->en_cnt;
	if (en_cnt != 1)
		goto out_ok;

	/* 1. enable bus-clks */
	if (!sw) {
		clk_prepare_enable(top->ahb_gate);
		clk_prepare_enable(top->ahb_reset);
		clk_prepare_enable(top->mem_axi_gate);
	}

	/* 2. enable work-clk which is source clk of inner-sclk */
	if (sclk->req_rate > 0)
		clk_set_rate(sclk->ext_sclk, sclk->req_rate);
	if (!sw)
		clk_prepare_enable(sclk->ext_sclk);
	real_rate = clk_get_rate(sclk->ext_sclk);

	/* 3. enable sclk of dpu-top */
	lombo_dpu_top_clk_assert(top, sw);
	if (sclk->use_adpll) {
		struct dpu_adpll_config *cfg = &(sclk->adpll_cfg);
		uint64_t tmp = sclk->req_rate;

		do_div(tmp, real_rate);
		cfg->n = (uint32_t)tmp;
		if (!sw) {
			ret = csp_dpu_adpll_enable(sclk->id, cfg);
			if (ret) {
				ret = -ETIMEDOUT;
				goto clk_deassert;
			}
			csp_dpu_set_sclk_parent(sclk->id,
						DPU_SCLK_PARENT_ADPLL);
		}

		real_rate *= cfg->n;
	}
	sclk->real_rate = real_rate;

out_ok:
	mutex_unlock(&sclk->mlock);

	DRM_DEBUG_KMS("sclk%d: en_cnt=%d, ret=%d\n", sclk->id, en_cnt, ret);
	WARN_ON(en_cnt <= 0);

	return 0;

clk_deassert:
	lombo_dpu_top_clk_deassert(top, sw);
	/* clk_disable: */
	if (!sw) {
		clk_disable_unprepare(sclk->ext_sclk);
		clk_disable_unprepare(top->mem_axi_gate);
		clk_disable_unprepare(top->ahb_reset);
		clk_disable_unprepare(top->ahb_gate);
	}
	sclk->en_cnt--;
	mutex_unlock(&sclk->mlock);
	DRM_ERROR("sclk%d@en_cnt=%d fail, ret=%d\n", sclk->id, en_cnt, ret);

	return ret;
}

static int lombo_dpu_disable_sclk(
	struct lombo_dpu_top *top, struct lombo_dpu_sclk *sclk)
{
	int en_cnt;

	mutex_lock(&sclk->mlock);
	sclk->en_cnt--;
	en_cnt = sclk->en_cnt;
	if (en_cnt != 0)
		goto out_ok;

	if (sclk->use_adpll) {
		csp_dpu_adpll_disable(sclk->id);
		csp_dpu_set_sclk_parent(sclk->id, DPU_SCLK_PARENT_EXT);
	}
	sclk->real_rate = 0;
	lombo_dpu_top_clk_deassert(top, 0);

	clk_disable_unprepare(sclk->ext_sclk);

	clk_disable_unprepare(top->mem_axi_gate);
	clk_disable_unprepare(top->ahb_reset);
	clk_disable_unprepare(top->ahb_gate);

out_ok:
	mutex_unlock(&sclk->mlock);
	DRM_DEBUG_KMS("sclk%d: en_cnt=%d\n", sclk->id, en_cnt);
	WARN_ON(en_cnt < 0);

	return 0;
}

static int lombo_dpu_enable_clk(struct lombo_dpu_top *top,
	struct lombo_dpu_clk *clk, int sw)
{
	struct lombo_dpu_sclk *sclk = clk->sclk;
	int en_cnt;
	uint64_t tmp;
	uint32_t div = 1;

	spin_lock(&clk->slock);
	clk->en_cnt++;
	en_cnt = clk->en_cnt;
	if (en_cnt != 1)
		goto out_ok;

	/* calc div */
	if (clk->req_rate > 0) {
		tmp = sclk->real_rate;
		if (clk->mod_id == DPU_MOD_TIMER)
			do_div(tmp, 1024);
		do_div(tmp, clk->req_rate);
		div = (uint32_t)tmp;
		div = div ? div : 1;
	}

	if (!sw) {
		spin_lock(&top->slock);
		csp_dpu_clk_assert(clk->mod_id);
		csp_dpu_clk_set_parent(clk->mod_id, sclk->id);
		csp_dpu_clk_set_divider(clk->mod_id, div);
		csp_dpu_clk_enable(clk->mod_id);
		spin_unlock(&top->slock);
	}

	tmp = sclk->real_rate;
	if (clk->mod_id == DPU_MOD_TIMER)
		do_div(tmp, div * 1024);
	else
		do_div(tmp, div);
	clk->real_rate = (unsigned long)tmp;

out_ok:
	spin_unlock(&clk->slock);

	DRM_DEBUG_KMS("dpu_mod[%d],en_cnt=%d\n", clk->mod_id, en_cnt);
	DRM_DEBUG_KMS("sclk_rate=%lu, req=%lu,real=%lu\n",
		      sclk->real_rate, clk->req_rate, clk->real_rate);
	WARN_ON(en_cnt <= 0);

	return 0;
}

static int lombo_dpu_disable_clk(
	struct lombo_dpu_top *top, struct lombo_dpu_clk *clk)
{
	int en_cnt;

	spin_lock(&clk->slock);
	clk->en_cnt--;
	en_cnt = clk->en_cnt;
	if (en_cnt == 0) {
		spin_lock(&top->slock);
		csp_dpu_clk_disable(clk->mod_id);
		csp_dpu_clk_deassert(clk->mod_id);
		spin_unlock(&top->slock);
		clk->real_rate = 0;
	}
	spin_unlock(&clk->slock);

	DRM_DEBUG_KMS("dpu_mod[%d], en_cnt=%d\n", clk->mod_id, en_cnt);
	WARN_ON(en_cnt < 0);

	return 0;
}

int lombo_dpu_clk_prepare_enable(struct lombo_dpu_clk *clk, int sw)
{
	int ret;

	if (!clk || !clk->top || !clk->sclk) {
		DRM_ERROR("null clk\n");
		return -EINVAL;
	}

	DRM_DEBUG_KMS("clk=%d,sw=%d\n", clk->mod_id, sw);

	ret = lombo_dpu_enable_sclk(clk->top, clk->sclk, sw);
	if (ret) {
		DRM_ERROR("enable sclk%d for clk(%d) fail, ret=%d\n",
			  clk->sclk->id, clk->mod_id, ret);
		return ret;
	}

	return lombo_dpu_enable_clk(clk->top, clk, sw);
}

int lombo_dpu_clk_disable_unprepare(struct lombo_dpu_clk *clk)
{
	if (!clk || !clk->top || !clk->sclk) {
		DRM_ERROR("null clk\n");
		return -EINVAL;
	}

	lombo_dpu_disable_clk(clk->top, clk);
	lombo_dpu_disable_sclk(clk->top, clk->sclk);

	return 0;
}

struct lombo_dpu_clk *lombo_dpu_get_enable_tclk(struct device *dpu_top,
	unsigned long req_rate, unsigned long *real_rate)
{
	struct lombo_dpu_clk *clk = lombo_dpu_get_clk(dpu_top,
						      dpu_top,
						      LOMBO_DPU_MOD_TIMER,
						      2);
	int ret;

	if (!clk)
		return NULL;

	if (req_rate)
		lombo_dpu_clk_set_rate(clk, req_rate);

	ret = lombo_dpu_clk_prepare_enable(clk, 0);
	if (ret) {
		lombo_dpu_put_clk(&clk);
		return NULL;
	}
	if (real_rate)
		*real_rate = lombo_dpu_clk_get_real_rate(clk);

	return clk;
}

static int lombo_dpu_parse_sclk(struct device *dev,
	struct lombo_dpu_top *top)
{
	char name[16] = { 0 };
	char name_1[16] = { 0 };
	int i;

	/* parse bus clks */
	top->ahb_gate = devm_clk_get(dev, "ahb_gate");
	if (IS_ERR(top->ahb_gate)) {
		DRM_DEV_ERROR(dev, "failed to get dpu ahb_gate\n");
		return -ENODEV;
	}
	top->ahb_reset = devm_clk_get(dev, "ahb_reset");
	if (IS_ERR(top->ahb_reset)) {
		DRM_DEV_ERROR(dev, "failed to get dpu ahb_reset\n");
		return -ENODEV;
	}
	top->mem_axi_gate = devm_clk_get(dev, "mem_axi_gate");
	if (IS_ERR(top->mem_axi_gate)) {
		DRM_DEV_ERROR(dev, "failed to get dpu mem_axi_gate\n");
		return -ENODEV;
	}

	/* parse ext sclk & psclk */
	strncpy(name, "ext_sclk0", sizeof(name));
	strncpy(name_1, "ext_psclk0", sizeof(name_1));
	for (i = 0; i < LOMBO_DPU_EXT_SCLK_NUM; i++) {
		struct clk *clk, *pclk;

		name[8] = i + '0';
		name_1[9] = i + '0';

		clk = devm_clk_get(dev, name);
		if (IS_ERR(clk))
			continue;
		top->ext_sclk[i] = clk;

		pclk = devm_clk_get(dev, name_1);
		if (!IS_ERR(pclk)) {
			clk_set_parent(clk, pclk);
			top->ext_psclk[i] = pclk;
		}
		DRM_DEV_DEBUG_DRIVER(dev, "has %s\n", name);
	}

	/* create inner sclk */
	strncpy(name, "sclk0", sizeof(name));
	strncpy(name_1, "sclk0_adpll", sizeof(name_1));
	for (i = 0; i < LOMBO_DPU_SCLK_NUM; i++) {
		uint32_t val;
		struct lombo_dpu_sclk *sclk = &(top->sclk[i]);

		name[4] = '0' + i;
		if (of_property_read_u32_index(dev->of_node, name, 0, &val)) {
			DRM_DEV_DEBUG(dev, "not ext_sclk for sclk%d\n", i);
			continue;
		}

		if (val >= LOMBO_DPU_EXT_SCLK_NUM) {
			DRM_DEV_ERROR(dev, "invalid ext_sclk for sclk%d\n", i);
			return -EINVAL;
		}

		sclk->ext_sclk = top->ext_sclk[val];
		sclk->id = i;

		if (!of_property_read_u32_index(dev->of_node, name, 1, &val))
			sclk->req_rate = val;

		name_1[4] = '0' + i;
		sclk->use_adpll = 0;
		if (!of_property_read_u32_index(dev->of_node,
						name_1, 0, &val)) {
			sclk->adpll_cfg.tune0 = val;
			sclk->use_adpll++;
		}
		if (!of_property_read_u32_index(dev->of_node,
						name_1, 1, &val)) {
			sclk->adpll_cfg.tune1 = val;
			sclk->use_adpll++;
		}
		sclk->use_adpll = (sclk->use_adpll == 2) ? 1 : 0;

		DRM_DEV_DEBUG_DRIVER(dev, "%s:req_rate=%lu, adpll=%d.",
			name, sclk->req_rate, sclk->use_adpll);
		DRM_DEV_DEBUG_DRIVER(dev, "adpll_tune[0x%x,0x%x]\n",
			sclk->adpll_cfg.tune0, sclk->adpll_cfg.tune1);

		mutex_init(&sclk->mlock);
	}

	return 0;
}

int lombo_dpu_set_wb_mode(unsigned int mode)
{
	return csp_dpu_set_wb_mode(mode);
}

static int lombo_dpu_bind(struct device *dev,
	struct device *master, void *master_data)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct drm_device *drm = master_data;
	struct lombo_drv *drv = drm->dev_private;
	struct lombo_dpu_top *top;
	struct resource *res;
	int ret;

	DRM_DEV_DEBUG_DRIVER(dev, "\n");
	top = devm_kzalloc(dev, sizeof(*top), GFP_KERNEL);
	if (!top)
		return -ENOMEM;
	drv->dpu_top = dev;
	dev_set_drvdata(dev, top);

	/* parse register */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	top->reg = devm_ioremap_resource(dev, res);
	if (IS_ERR(top->reg))
		return PTR_ERR(top->reg);
	csp_dpu_set_register_base(top->reg);

	ret = lombo_dpu_parse_sclk(dev, top);
	if (ret)
		return ret;

	INIT_LIST_HEAD(&top->clk_list);
	spin_lock_init(&top->slock);

	return 0;
}

static void lombo_dpu_unbind(struct device *dev,
	struct device *master, void *master_data)
{
	DRM_DEV_DEBUG_DRIVER(dev, "\n");
	csp_dpu_set_register_base(NULL);
	dev_set_drvdata(dev, NULL);
}

static const struct component_ops dpu_comp_ops = {
	.bind	= lombo_dpu_bind,
	.unbind	= lombo_dpu_unbind,
};

static int lombo_dpu_probe(struct platform_device *pdev)
{
	DRM_DEV_DEBUG_DRIVER(&pdev->dev, "%d\n", __LINE__);
	return component_add(&pdev->dev, &dpu_comp_ops);
}

static int lombo_dpu_remove(struct platform_device *pdev)
{
	DRM_DEV_DEBUG_DRIVER(&pdev->dev, "%d\n", __LINE__);
	component_del(&pdev->dev, &dpu_comp_ops);
	return 0;
}

static const struct of_device_id dpu_of_table[] = {
	{ .compatible = "lombo,lombo-n5v1-dpu" },
	{ .compatible = "lombo,lombo-n7v1-dpu" },
	{ .compatible = "lombo,lombo-n7v3-dpu" },
	{ .compatible = "lombo,lombo-n7v5-dpu" },
	{ .compatible = "lombo,lombo-n9v1-dpu" },
	{ }
};
MODULE_DEVICE_TABLE(of, dpu_of_table);

static struct platform_driver lombo_dpu_platform_driver = {
	.probe		= lombo_dpu_probe,
	.remove		= lombo_dpu_remove,
	.driver		= {
		.owner = THIS_MODULE,
		.name		= "lombo-dpu",
		.of_match_table	= dpu_of_table,
	},
};
module_platform_driver(lombo_dpu_platform_driver);

MODULE_AUTHOR("lomboswer <lomboswer@lombotech.com>");
MODULE_DESCRIPTION("Lombo SoC DPU TOP Driver");
MODULE_LICENSE("GPL");
